// $Id: buffers.c 64 2011-04-23 11:35:44Z cedric.shih@gmail.com $
/*
 * Copyright (C) 2011 Cedric Shih <cedric.shih@gmail.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

#include "log.h"

#include "buffers.h"

#define niobuffers_lock(buffers) do { \
	if (buffers->lock && (errno = pthread_mutex_lock(buffers->lock)) != 0) { \
		rc = errno; \
		nioerror("failed to lock: %s", strerror(rc)); \
		return rc; \
	} \
} while(0);

#define niobuffers_unlock(buffers) do { \
	if (buffers->lock && (errno = pthread_mutex_unlock(buffers->lock)) != 0) { \
		rc = errno; \
		nioerror("failed to unlock: %s", strerror(rc)); \
		return rc; \
	} \
} while(0);

struct niobuffers *niobuffers_new(enum niobuffers_type type,
		unsigned int capacity)
{
	struct niobuffers *buffers;

	niodebug("constructing");

	if ((buffers = calloc(1, sizeof(struct niobuffers))) == NULL) {
		nioerror("failed to allocate memory");
		return NULL;
	}

	if ((buffers->buffer = malloc(capacity * sizeof(u_char))) == NULL) {
		nioerror("failed to allocate buffers: %d bytes", capacity);
		goto exception;
	}

	buffers->offset = capacity;
	TAILQ_INIT(&buffers->list);
	TAILQ_INIT(&buffers->cache);

	buffers->boundary = buffers->buffer + buffers->offset;
	buffers->start = buffers->buffer;

	return buffers;

exception:
	niobuffers_free(buffers);
	return NULL;
}

void niobuffers_clear_list(struct niobuffer_list *list)
{
	struct niobuffer_item *item;

	while ((item = TAILQ_FIRST(list))) {
		TAILQ_REMOVE(list, item, entry);
		niobuffer_free(item->buffer);
		free(item->buffer);
		free(item);
	}
}

void niobuffers_free(struct niobuffers *buffers)
{
	if (buffers == NULL) {
		return;
	}

	niodebug("destructing");

	if (!TAILQ_EMPTY(&buffers->list)) {
		niodebug("clearing buffers");

		niobuffers_clear_list(&buffers->list);
	}

	if (!TAILQ_EMPTY(&buffers->cache)) {
		niodebug("clearing buffer caches");

		niobuffers_clear_list(&buffers->cache);
	}

	if (buffers->buffer) {
		free(buffers->buffer);
	}

	free(buffers);
}

void niobuffers_set_lock(struct niobuffers *buffers, pthread_mutex_t *lock)
{
	buffers->lock = lock;
}

struct niobuffer *niobuffers_create(
		struct niobuffers *buffers, size_t len)
{
	int rc;
	struct niobuffer *buffer = NULL;

	niotrace("creating %d bytes of buffer", len);

	if ((rc = niobuffers_alloc(buffers, len, &buffer))) {
		nioerror("failed to alloc: %s", strerror(rc));
		errno = rc;
	}

	return buffer;
}

size_t niobuffers_available(struct niobuffers *buffers)
{
	size_t buffers_free;

	niotrace("calculating available space");

	if (buffers->stat.count == 0) {
		buffers_free = buffers->offset;
	} else if (buffers->next <= buffers->start) {
		buffers_free = buffers->start - buffers->next;
	} else if (buffers->start == buffers->buffer) {
		buffers_free = buffers->boundary - buffers->next;
	} else {
		buffers_free = buffers->start - buffers->buffer;
	}

	niotrace("available: %u bytes", buffers_free);

	return buffers_free;
}

int niobuffers_release(struct niobuffers *buffers,
		struct niobuffer *buffer)
{
	int rc;

	niobuffers_lock(buffers);

	if (buffer->type != NIO_BUFFER_ROTATED ||
			buffer->source.factory != buffers) {
		nioerror("not created from this");
		rc = EINVAL;
		goto finally;
	}

	niotrace("releasing %d bytes of buffer", buffer->capacity);

	if (buffer == buffers->current->buffer) {
		niotrace("updating index: %p => %p",
				buffers->current,
				TAILQ_NEXT(buffers->current, entry));
		buffers->current = TAILQ_NEXT(buffers->current, entry);
	}

	if (!TAILQ_EMPTY(&buffers->list) &&
			(buffer == TAILQ_FIRST(&buffers->list)->buffer)) {
		if ((rc = niobuffers_remove_first(buffers))) {
			nioerror("failed to spare: %s", strerror(rc));
			goto finally;
		}
	} else if (!TAILQ_EMPTY(&buffers->list) &&
			(buffer == TAILQ_LAST(
			&buffers->list, niobuffer_list)->buffer)) {
		if ((rc = niobuffers_remove_last(buffers))) {
			nioerror("failed to spare: %s", strerror(rc));
			goto finally;
		}
	} else {
		niotrace("marking released");
		buffer->released = 1;
	}

	rc = 0;

finally:
	niobuffers_unlock(buffers);
	return rc;
}

int niobuffers_remove_first(struct niobuffers *buffers)
{
	int rc;
	struct niobuffer_item *first, *next;

	if (!(first = TAILQ_FIRST(&buffers->list))) {
		nioerror("nothing to remove");
		rc = ENOENT;
		goto finally;
	}

	niotrace("removing first buffer");

	if ((next = TAILQ_NEXT(first, entry))) {
		buffers->start = next->buffer->data;
	} else {
		buffers->start = buffers->buffer;
	}

	if (buffers->current == first) {
		buffers->current = next;
	}

	TAILQ_REMOVE(&buffers->list, first, entry);
	TAILQ_INSERT_TAIL(&buffers->cache, first, entry);

	buffers->stat.count--;
	buffers->stat.dropped++;

	niotrace("released from first buffer: %p-%p (%u bytes)",
			first->buffer->data,
			(first->buffer->data + first->buffer->capacity - 1),
			first->buffer->capacity);
	rc = 0;

finally:
	return rc;
}

int niobuffers_remove_last(struct niobuffers *buffers)
{
	int rc;
	struct niobuffer_item *last, *prev;

	if (!(last = TAILQ_LAST(&buffers->list, niobuffer_list))) {
		nioerror("nothing to remove");
		rc = ENOENT;
		goto finally;
	}

	niotrace("removing last buffer from buffers");

	if ((prev = TAILQ_PREV(last, niobuffer_list, entry))) {
		buffers->next = prev->buffer->data;
	} else {
		buffers->next = buffers->buffer;
	}

	if (buffers->current == last) {
		buffers->current = NULL;
	}

	TAILQ_REMOVE(&buffers->list, last, entry);
	TAILQ_INSERT_TAIL(&buffers->cache, last, entry);

	buffers->stat.count--;
	buffers->stat.dropped++;

	niotrace("released from last buffer: %p-%p (%u bytes)",
			last->buffer->data,
			(last->buffer->data + last->buffer->capacity - 1),
			last->buffer->capacity);
	rc = 0;

finally:
	return rc;
}

int niobuffers_reset(struct niobuffers *buffers)
{
	int rc;
	struct niobuffer_item *item;

	niobuffers_lock(buffers);

	niotrace("resetting buffers");

	while ((item = TAILQ_FIRST(&buffers->list))) {
		TAILQ_REMOVE(&buffers->list, item, entry);
		TAILQ_INSERT_TAIL(&buffers->cache, item, entry);
	}

	buffers->current = NULL;
	buffers->stat.count = buffers->stat.put = buffers->stat.dropped = 0;
	buffers->boundary = buffers->buffer + buffers->offset;
	buffers->start = buffers->next = buffers->buffer;

	rc = 0;

	niobuffers_unlock(buffers);
	return rc;
}

inline int niobuffers_unread(struct niobuffers *buffers,
		struct niobuffer **ptr)
{
	int rc;

	niobuffers_lock(buffers);

	niotrace("getting unread buffer: %p", buffers->current);

	if (!buffers->current) {
		niotrace("no unread buffer");
	} else {
		*ptr = buffers->current->buffer;
		niotrace("unread: %p at %p-%p (%u bytes)",
				(*ptr), (*ptr)->data,
				((*ptr)->data + (*ptr)->capacity - 1),
				(*ptr)->capacity);

		niotrace("updating current: %p => %p", buffers->current,
				TAILQ_NEXT(buffers->current, entry));

		buffers->current = TAILQ_NEXT(buffers->current, entry);
	}

	rc = 0;

	niobuffers_unlock(buffers);
	return rc;
}

struct niobuffer *niobuffers_read(struct niobuffers *buffers)
{
	struct niobuffer *next = NULL;
	niobuffers_unread(buffers, &next);
	return next;
}

int niobuffers_stat(struct niobuffers *buffers,
		struct niobuffers_stat *stat)
{
	int rc;

	niobuffers_lock(buffers);

	niotrace("getting statistics");

	memcpy(stat, &buffers->stat, sizeof(buffers->stat));
	rc = 0;

	niobuffers_unlock(buffers);
	return rc;
}

int niobuffers_alloc(struct niobuffers *buffers, size_t len,
		struct niobuffer **ptr)
{
	int rc;
	struct niobuffer *buffer = NULL;
	struct niobuffer_item *item = NULL;

	niobuffers_lock(buffers);

	if (len > buffers->offset) {
		nioerror("insufficient capacity");
		rc = ENOMEM;
		goto finally;
	}

	while (niobuffers_available(buffers) < len) {
		if ((rc = niobuffers_remove_first(buffers)) != 0) {
			nioerror("failed to spare space: %s",
					strerror(rc));
			goto finally;
		}
	}

	niotrace("allocating %d bytes", len);

	if (TAILQ_EMPTY(&buffers->cache)) {
		if (!(buffer = calloc(1, sizeof(*buffer)))) {
			rc = errno ? errno : -1;
			nioerror("failed to calloc: %s", strerror(rc));
			goto finally;
		}

		if ((rc = niobuffer_list_add(&buffers->cache, buffer))) {
			nioerror("failed to add: %s", nio_strerror(rc));
			goto finally;
		}
	}

	item = TAILQ_FIRST(&buffers->cache);
	TAILQ_REMOVE(&buffers->cache, item, entry);
	buffer = item->buffer;

	TAILQ_INIT(&buffer->wrapped);
	TAILQ_INIT(&buffer->cloned);

	if (buffers->stat.count == 0 ||
			buffers->boundary - buffers->next < len) {
		buffers->next = buffers->buffer;
	}

	niotrace("buffer: %p", buffer);

	buffer->type = NIO_BUFFER_ROTATED;
	buffer->source.factory = buffers;
	buffer->data = buffers->next;
	buffer->limit = buffer->capacity = len;
	buffer->position = buffer->mark = 0;

	if (!buffers->current) {
		buffers->current = item;
	}

	niotrace("allocated: %p-%p (%u bytes)",
			buffer->data, (buffer->data + buffer->capacity - 1),
			buffer->capacity);

	TAILQ_INSERT_TAIL(&buffers->list, item, entry);
	buffers->stat.count++;
	buffers->stat.put++;
	buffers->next += len;

	niotrace("count: %d", buffers->stat.count);

	*ptr = buffer;
	rc = 0;

finally:
	if (rc != 0) {
		if (buffer) {
			niobuffers_remove_last(buffers);
		}
	}
	niobuffers_unlock(buffers);
	return rc;
}

void niobuffer_set_cb(struct niobuffer *buffer,
		niobuffer_cb cb, void *cbarg)
{
	buffer->cb = cb;
	buffer->cbarg = cbarg;
}
